package com.friend.uu;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.provider.OpenableColumns;
import android.webkit.MimeTypeMap;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public class P2pService extends Service {
    public static final String ACTION_SEND = "com.friend.uu.action.SEND";
    public static final String ACTION_RECEIVE = "com.friend.uu.action.RECEIVE";
    public static final String EXTRA_FILE = "SHARE FILE";
    public static final String EXTRA_IP = "IP";
    public static final String EXTRA_PORT = "PORT";
    public static final String EXTRA_KEY = "KEY";
    private final Handler mHandler = new Handler();
    private boolean needUpdate;
    private ServerSocket mServerSocket;
    private int mNId;

    public P2pService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();


    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mServerSocket != null) {
            try {
                mServerSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String action = intent.getAction();
        if (action != null && action.equals(ACTION_SEND)) {
            // Send file
            final Uri fileUri = Uri.parse(intent.getStringExtra(EXTRA_FILE));
            final String ip = intent.getStringExtra(EXTRA_IP);
            final int port = intent.getIntExtra(EXTRA_PORT, 8888);
            final byte key = intent.getByteExtra(EXTRA_KEY, (byte) 22);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    startSendFile(fileUri, ip, port, key);
                }
            }).start();
        } else if (action != null && action.equals(ACTION_RECEIVE)) {
            // Open Receiver
            final int port = intent.getIntExtra(EXTRA_PORT, 8888);
            final byte key = intent.getByteExtra(EXTRA_KEY, (byte) 22);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    openServer(port, key);
                }
            }).start();
        }
        return super.onStartCommand(intent, flags, startId);
    }

    private void startSendFile(Uri fileUri, String ip, int port, byte key) {
        InputStream in = null;
        Socket socket = null;
        try (Cursor cur = getContentResolver().query(fileUri,
                new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE},
                null, null, null)) {
            cur.moveToFirst();
            String fileName = cur.getString(0);
            String ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(
                    getContentResolver().getType(fileUri));
            if (ext != null && !fileName.endsWith("." + ext)) {
                fileName += "." + ext;
            }
            long length = cur.getLong(1);
            byte[] filenameBytes = fileName.getBytes(StandardCharsets.UTF_8);
            int fileNameLength = filenameBytes.length;
            if (fileNameLength > 255) {
                showMessage(getString(R.string.filename_too_long), true);
                return;
            }
            socket = new Socket();
            socket.connect(new InetSocketAddress(ip, port), 10000);
            in = getContentResolver().openInputStream(fileUri);
            OutputStream out = socket.getOutputStream();
            // key
            out.write(key);
            // filename length
            out.write((byte) fileNameLength);
            // filename
            out.write(filenameBytes);
            // file length
            byte[] lengthBytes = longToBytes(length);
            out.write(lengthBytes);
            // content
            transferStream(in, out, length, fileName);
            showMessage(getString(R.string.send_finished, fileName), false);
        } catch (Exception e) {
            showMessage(e.getMessage(), true);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket != null) {
                if (socket.isConnected()) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            stopSelf();
        }
    }

    private void openServer(int port, byte key) {
        OutputStream out = null;
        Socket socket = null;
        File destFile = null;
        boolean isError = false;
        try {
            mServerSocket = new ServerSocket(port);
            mServerSocket.setSoTimeout(5 * 60 * 1000);// 5 minutes
            showMessage(getString(R.string.waiting_receive), false);
            socket = mServerSocket.accept();
            InputStream in = socket.getInputStream();
            // key
            if (key != in.read()) {
                throw new Exception(getString(R.string.share_key_error));
            }
            // filename length
            int fileNameLength = in.read();
            // filename
            byte[] filenameBytes = new byte[fileNameLength];
            //noinspection ResultOfMethodCallIgnored
            in.read(filenameBytes);
            String fileName = new String(filenameBytes, StandardCharsets.UTF_8);
            // data length
            byte[] lengthBytes = new byte[8];
            //noinspection ResultOfMethodCallIgnored
            in.read(lengthBytes);
            long length = bytesToLong(lengthBytes);
            destFile = new File(Environment.getExternalStoragePublicDirectory(
                    Environment.DIRECTORY_DOWNLOADS), fileName);
            out = new FileOutputStream(destFile);
            // content
            transferStream(in, out, length, fileName);
            showMessage(getString(R.string.receive_finished,
                    destFile.getAbsolutePath()), false);
        } catch (Exception e) {
            isError = true;
            showMessage(e.getMessage(), true);
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket != null && socket.isConnected()) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (mServerSocket != null) {
                try {
                    mServerSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (isError) {
                stopSelf();
            } else {
                if (destFile != null) {
                    //noinspection ResultOfMethodCallIgnored
                    destFile.delete();
                }
                openServer(port, key);
            }
        }
    }

    private void transferStream(InputStream in, OutputStream out, long length,
                                String fileName) {
        // init notification
        NotificationManager manager =
                (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        Notification.Builder builder =
                new Notification.Builder(getApplicationContext());
        // Set Notification Channel for API 26
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            String cid = getPackageName() + ".p2p.notification.channel";
            String cn = "P2P Channel";
            int imp = NotificationManager.IMPORTANCE_DEFAULT;
            NotificationChannel channel = new NotificationChannel(cid, cn, imp);
            manager.createNotificationChannel(channel);
            builder.setChannelId(cid);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            builder.setCategory("P2P");
        }
        Intent intent = new Intent(getApplicationContext(), P2pActivity.class);
        builder.setSmallIcon(R.drawable.ic_p2p)
                .setTicker("P2P: " + fileName)
                .setContentTitle("P2P: " + fileName)
                .setContentIntent(PendingIntent.getActivity(
                        getApplicationContext(), 0, intent, 0))
                .setOnlyAlertOnce(true);

        // start timer
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                needUpdate = true;
            }
        }, 0, 1000);

        // start transfer
        int nid = mNId++;
        int len;
        long written = 0;
        int speed = 0;
        byte[] buf = new byte[4096];
        try {
            while ((len = in.read(buf)) != -1) {
                out.write(buf, 0, len);
                written += len;
                speed += len;
                if (needUpdate) {
                    needUpdate = false;
                    int p = (int) (written * 100 / length);
                    builder.setProgress(100, p, false);
                    String state = String.format(Locale.getDefault(),
                            "%1.2fMB/s  %d%%",
                            speed / 1024 / 1024f, p);
                    builder.setContentText(state);
                    manager.notify(nid, builder.build());
                    speed = 0;
                }
            }
            builder.setContentText("Finished!")
                    .setProgress(0, 0, false);
            manager.notify(nid, builder.build());
        } catch (IOException e) {
            builder.setContentText("Error: " + e.getMessage())
                    .setProgress(0, 0, false);
            manager.notify(nid, builder.build());
        } finally {
            timer.cancel();
        }
    }

    private byte[] longToBytes(long val) {
        byte[] bs = new byte[8];
        for (int i = 0; i < bs.length; i++) {
            // 无符号>>>右移，高位补零
            bs[i] = (byte) (val >>> (bs.length - 1 - i) * 8);
        }
        return bs;
    }

    private long bytesToLong(byte[] bs) {
        long val = 0;
        for (byte b : bs) {
            // & 0xff 去掉符号位，否则 | 运算会先把b转换为带符号位long再计算
            val = (val << 8) | (b & 0xff);
        }
        return val;
    }

    private void showMessage(final String msg, final boolean isError) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                String message = isError ? "e: " + msg : msg;
                Toast.makeText(getApplicationContext(), message,
                        Toast.LENGTH_LONG).show();
            }
        });
    }
}